Шрифт:
Интервал:
Закладка:
Резюмируя, объявление блока кода синхронным обеспечивает для кода атомарность и видимость.
Атомарность значит, что только один поток одновременно может выполнять код, защищенный данным объектом-монитором (блокировкой), позволяя предотвратить многочисленные потоки от столкновений друг с другом во время обновления общего состояния.
Видимость связана с особенностями кэширования памяти и оптимизацией программы в процессе компилирования.
Обычно потоки могут свободно кэшировать значения для переменных так, чтобы они не обязательно сразу же были бы видны другим потокам, но, если разработчик использовал синхронизацию, во время выполнения будет проверяться, что обновления переменных, выполненные одним потоком до выхода из синхронизированного блока, сразу же будут видны другому потоку, когда он будет входить в синхронизированный блок, защищенный тем же монитором (блокировкой).
Подобное правило видимости существует и для переменных volatile.
Живучесть Liveness
Теперь, давайте рассмотрим фундаментальное свойство корректности многопоточных программ, которое называется LIVENESS или живучесть.
Когда вы пишете не многопоточную, а последовательную программу, и в ней есть ошибка, вы запускаете ее, и на экране ничего не появляется.
Вы ждете, а затем выясняете, например, что у вас в коде есть бесконечный цикл.
И тогда вы исправляете ошибку.
К сожалению, в многопоточных программах есть много других способов получить этот эффект пустого экрана.
Один из них — это DEADLOCK или взаимная блокировка.
Это мы уже видели в случае операции join.
Если два потока соединяются друг с другом, они создают цикл взаимоблокировки, и по этой причине программа не завершается.
Это не бесконечный цикл в обычном понимании, это два потока, заблокированных друг от друга на неопределенный срок.
Существуют и другие способы получения взаимоблокировки.
Например, если поток T1 выполняет синхронизированную операцию на объекте A и вложенную синхронизированную операцию на объекте B, а поток T2 выполняет синхронизированную операцию на объекте B и вложенную синхронизированную операцию на объекте A, мы получаем другую форму взаимоблокировки.
Поток T1 может получить монитор объекта A одновременно с тем, что поток T2 получит монитор объекта B, а затем каждый поток будет ожидать монитора В и А соответственно неопределенный срок.
Одним из лучших способов предотвращения взаимоблокировки — это избегать одновременного получения более одного монитора.
Еще одно нарушение живучести, это LIVELOCK или динамическая взаимоблокировка.
В livelock потоки не блокируются, но они находятся в режиме, в котором их выполнение не продвигается дальше, это похоже на пат в шахматной игре.
Например, если у нас есть объект, скажем, изменяемая целочисленная переменная x, и у нас есть два потока.
Поток T1 в цикле увеличивает x, затем читает значение x и продолжает делать это, пока х меньше 2.
А поток T2 в цикле уменьшает значение x, затем читает значение x и продолжает делать это, пока х больше -2.
Возможна ситуация, при которой поток T1 получает x = 1, но прежде чем он получит шанс увеличить x и достичь x = 2, поток T2 уменьшает x, противодействуя тому, что делает T1.
И делает x = -1.
Но до того, как поток T2 получит шанс уменьшить х до -2, поток T1 может снова увеличить x до 1.
И так до бесконечности.
Таким образом, значение х может двигаться вперед и назад, как непрерывный бесконечный пинг-понг.
Теперь третий вид проблемы с живучестью, называется STARVATION или голодание.
Starvation возникает, когда какой-либо поток не может получить доступ к общим ресурсам и не может быть выполнен в результате, например, синхронизированного доступа к этому ресурсу другими потоками, выполнение которых занимает долгое время.
В результате этот поток голодает.
Паттерн защищенный блок Guarded Block
Предположим у нас есть задача написать приложение Producer-Consumer.
Это приложение состоит из двух потоков — производителя, который создает данные, и потребителя, который что-то делает с этими данными.
Два потока обмениваются данными с использованием общего объекта.
При этом потребительский поток не должен пытаться извлекать данные до тех пор, пока поток производителя не создаст данные, а поток производителя не должен пытаться оставить новые данные, пока потребитель не извлечет старые данные.
Для создания такого приложения используется паттерн Защищенный блок.
Сначала значение empty установлено в true.
Поток потребителя вызывает синхронизированный метод take.
Объект класса блокируется.
Метод запускает цикл while.
В этом цикле вызывается метод wait.
При этом объект класса разблокируется.
Поток производитель вызывает метод put.
При этом объект класса блокируется.
Блок while метода put пропускается.
Записывается сообщение и значение empty устанавливается в false.
Вызывается метод notifyAll, который будит все потоки.
Монитор объекта перехватывается потоком потребителем.
Цикл while завершается, и значение empty устанавливается снова в true.
Сообщение потребляется.
Таким образом, паттерн защищенный блок обеспечивает координацию действий потоков.
При использовании этого паттерна не забудьте убедиться, что приложение имеет другие потоки, которые получают блокировки на объектах, на которых другие потоки ранее вызывали метод wait, и эти другие потоки вызывают методы notify или notifyAll, чтобы приложение могло избежать startvation для тех потоков, которые вызвали метод wait.
Метод notifyAll будит все ожидающие потоки, тогда как метод notify случайно выбирает один из ожидающих потоков и будит его.
Теперь вопрос.
Можно ли написать такой класс, чтобы состояние объекта такого класса не могло быть изменено после создания объекта, и таким образом, так как такой объект не может изменять состояние, он не может быть поврежден интерференцией потоков или быть в несогласованном состоянии.
Для этого нужно сделать следующее:
Не давать классу методы «setter» — методы, которые изменяют поля или объекты, на которые ссылаются поля.
Сделать все поля класса финальными и приватными.
Не допустить создание подклассов с помощью объявления класса финальным.
Не давать классу методы, изменяющие изменяемые объекты.
Создание объектов неизменяемыми, является хорошей практикой в некоторых случаях, и помогает создать потокобезопасный код.
Интерфейс Lock
Пакет java.util.concurrent представляет API более высокого уровня для синхронизации, координации и управления потоками.
В частности, это интерфейс Lock.
Обеспечивает управление доступом к общему ресурсу для нескольких потоков.
Основное отличие интерфейса Lock от использования низкоуровневого API в виде synchronized, wait, notify, и volatile, это:
Возможность запроса блокировки до тех пор, пока текущий поток не прервется. С простой синхронизацией невозможно прервать поток, который